<!DOCTYPE html>
<html class="has-navbar-fixed-top">
<head>
    <meta charset="utf-8">
<title>单元测试 - wanzixin</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/outdated-browser/1.1.5/outdatedbrowser.min.css">


<link href="/Study/Java/zh-cn/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/" rel="alternate" hreflang="en" />
    


<meta name="description" content="">





    <meta name="description" content="本节我们介绍Java平台最常用的测试框架JUnit，并详细介绍如何编写单元测试。">
<meta property="og:type" content="article">
<meta property="og:title" content="单元测试">
<meta property="og:url" content="https://wanzixin.github.io/Study/Java/zh-cn/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/index.html">
<meta property="og:site_name" content="wanzixin">
<meta property="og:description" content="本节我们介绍Java平台最常用的测试框架JUnit，并详细介绍如何编写单元测试。">
<meta property="og:locale" content="zh_CN">
<meta property="article:published_time" content="2021-05-27T11:38:55.000Z">
<meta property="article:modified_time" content="2021-05-30T14:24:51.435Z">
<meta property="article:author" content="wanzixin">
<meta name="twitter:card" content="summary">





<link rel="icon" href="/favicon.png">


<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Ovo|Source+Code+Pro">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css">


<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/lightgallery/1.6.8/css/lightgallery.min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/justifiedGallery/3.6.5/css/justifiedGallery.min.css">


<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/atom-one-light.min.css">


<link rel="stylesheet" href="/css/style.css">


<script defer src="//use.fontawesome.com/releases/v5.0.8/js/all.js"></script>


    
    
    
    
    
    
    
    
    
    

    


<meta name="generator" content="Hexo 5.4.0"></head>
<body>
    
<nav class="navbar is-transparent is-fixed-top navbar-main" role="navigation" aria-label="main navigation">
    <div class="container">
        <div class="navbar-brand">
            <a class="navbar-item navbar-logo" href="/zh-cn">
                
                    
                    wanzixin
                    
                
            </a>
            <div class="navbar-burger">
                <span></span>
                <span></span>
                <span></span>
            </div>
        </div>
        
        <div class="navbar-menu navbar-start">
            
            <a class="navbar-item "
               href="/archives">Archives</a>
            
            <a class="navbar-item "
               href="/categories">Categories</a>
            
            <a class="navbar-item "
               href="/categories/Diary">Diary</a>
            
            <a class="navbar-item "
               href="/categories/Gallery">Gallery</a>
            
            <a class="navbar-item "
               href="/categories/Study">Study</a>
            
            <a class="navbar-item "
               href="/categories/Item">Item</a>
            
            <a class="navbar-item "
               href="/about">About</a>
            
        </div>
        
        <div class="navbar-menu navbar-end">
            
            <a class="navbar-item search" title="搜索" href="javascript:;">
                <i class="fas fa-search"></i>
            </a>
            
            
            <div class="navbar-item is-hoverable has-dropdown is-hidden-mobile is-hidden-tablet-only toc">
                <a class="navbar-item" title="目录">
                    <i class="fa fa-list"></i>
                </a>
                <div class="navbar-dropdown is-right">
                    
                    
                    
                    
                    <a class="navbar-item" href="#编写JUnit单元测试">1&nbsp;&nbsp;<b>编写JUnit单元测试</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#JUnit">1.1&nbsp;&nbsp;JUnit</a>
                    
                    
                    
                    <a class="navbar-item" href="#单元测试的好处">1.2&nbsp;&nbsp;单元测试的好处</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#使用Fixture">2&nbsp;&nbsp;<b>使用Fixture</b></a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#异常测试">3&nbsp;&nbsp;<b>异常测试</b></a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#条件测试">4&nbsp;&nbsp;<b>条件测试</b></a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#参数化测试">5&nbsp;&nbsp;<b>参数化测试</b></a>
                    
                </div>
            </div>
            
            
            <a class="navbar-item" title="GitHub" target="_blank" rel="noopener" href="https://github.com/wanzixin">
                
                <i class="fab fa-github"></i>
                
            </a>
               
            
        </div>
    </div>
</nav>

    <section class="section">
    <div class="container">
    <article class="article content gallery" itemscope itemprop="blogPost">
    <h1 class="article-title is-size-3 is-size-4-mobile" itemprop="name">
        
            单元测试
        
    </h1>
    <div class="article-meta columns is-variable is-1 is-multiline is-mobile is-size-7-mobile">
        <span class="column is-narrow">
            
                <span>5月 27 2021</span>
            
        </span>
        
        <span class="column is-narrow article-category">
            <i class="far fa-folder"></i>
            <a class="article-category-link" href="/zh-cn/categories/Study/">Study</a><span>></span><a class="article-category-link" href="/zh-cn/categories/Study/Java/">Java</a>
        </span>
        
        
        <span class="column is-narrow">
            
            
            25 分钟 读完 (约 3700 字)
        </span>
        
    </div>
    <div class="article-entry is-size-6-mobile" itemprop="articleBody">
    
        <html><head></head><body><p>本节我们介绍Java平台最常用的测试框架JUnit，并详细介绍如何编写单元测试。<span id="more"></span></p>
<h2 id="编写JUnit单元测试"><a href="#编写JUnit单元测试" class="headerlink" title="编写JUnit单元测试"></a>编写JUnit单元测试</h2><p>大部分情况是我们编写好了实现代码，需要对已有的代码进行测试。要测试一个方法，一个很自然的想法是编写一个main()方法，然后运行一些测试代码。不过，使用main()方法测试有很多缺点：一是只能有一个main()方法，不能把测试代码分离；二是没有打印出测试结果和期望结果，例如，<code>expected: 3628800, but actual: 123456</code>；三是很难编写一组通用的测试代码。</p>
<p>因此，我们需要一种测试框架，帮助我们编写测试。</p>
<h3 id="JUnit"><a href="#JUnit" class="headerlink" title="JUnit"></a>JUnit</h3><p>JUnit是一个开源的Java语言的单元测试框架，专门针对Java开发，使用最广泛。JUnit是事实上的单元测试的标准框架，任何Java开发者都应当学习并使用JUnit编写单元测试。</p>
<p>使用JUnit编写单元测试的好处在于，我们可以非常简单地组织测试代码，并随时运行它们，JUnit就会给出成功的测试和失败的测试，还可以生成测试报告，不仅包含测试的成功率，还可以统计测试的代码覆盖率，即被测试的代码本身有多少经过了测试。对于高质量的代码来说，测试覆盖率应该在80%以上。</p>
<p>几乎所有的IDE都集成了JUnit，这样我们就可以直接在IDE中编写并运行JUnit测试。</p>
<p>假定我们编写了一个计算阶乘的类，它只有一个静态方法来计算阶乘：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Factorial</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">long</span> <span class="hljs-title">fact</span><span class="hljs-params">(<span class="hljs-keyword">long</span> n)</span> </span>{<br>        <span class="hljs-keyword">long</span> r = <span class="hljs-number">1</span>;<br>        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">long</span> i = <span class="hljs-number">1</span>; i &lt;= n; i++) {<br>            r = r * i;<br>        }<br>        <span class="hljs-keyword">return</span> r;<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>我们来看一下<code>FactorialTest.java</code>的内容：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">package</span> xxx;<br><br><span class="hljs-keyword">import</span> <span class="hljs-keyword">static</span> org.junit.jupiter.api.Assertions.*;<br><span class="hljs-keyword">import</span> org.junit.jupiter.api.Test;<br><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FactorialTest</span> </span>{<br><br>    <span class="hljs-meta">@Test</span><br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">testFact</span><span class="hljs-params">()</span> </span>{<br>        assertEquals(<span class="hljs-number">1</span>, Factorial.fact(<span class="hljs-number">1</span>));<br>        assertEquals(<span class="hljs-number">2</span>, Factorial.fact(<span class="hljs-number">2</span>));<br>        assertEquals(<span class="hljs-number">6</span>, Factorial.fact(<span class="hljs-number">3</span>));<br>        assertEquals(<span class="hljs-number">3628800</span>, Factorial.fact(<span class="hljs-number">10</span>));<br>        assertEquals(<span class="hljs-number">2432902008176640000L</span>, Factorial.fact(<span class="hljs-number">20</span>));<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>核心测试方法加上<code>@Test</code>注解，这是JUnit要求的，它会把带有<code>@Test</code>的方法识别为测试方法。在测试方法内部，我们用<code>assertEquals(1, Factorial.fact(1))</code>表示期望<code>Factorial.fact(1)</code>返回<code>1</code>。<code>assertEquals(expected, actual)</code>是最常用的测试方法，它在<code>Assertion</code>类中定义。<code>Assertion</code>还定义了其他断言方法，例如：</p>
<ul>
<li><code>assertTrue()</code>: 期待结果为<code>true</code></li>
<li><code>assertFalse()</code>: 期待结果为<code>false</code></li>
<li><code>assertNotNull()</code>: 期待结果为非<code>null</code></li>
<li><code>assertArrayEquals()</code>: 期待结果为数组并与期望数组每个元素的值均相等</li>
<li>…</li>
</ul>
<p>运行单元测试非常简单，选中<code>FactorialTest.java</code>文件，右键点击，<code>Run As JUnit Test</code>，IDE会自动运行这个测试，并显示结果。如果结果与预期不符，<code>assertEquals()</code>会抛出异常，我们就会得到一个测试失败的结果。在Failure Trace中，JUnit会告诉我们详细的错误结果。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs java">org.opentest4j.AssertionFailedError: expected: &lt;<span class="hljs-number">3628800</span>&gt; but was: &lt;<span class="hljs-number">362880</span>&gt;<br>...<br></code></pre></td></tr></tbody></table></figure>

<p>第一行的失败信息的意思是期待结果3628800，但是实际返回362880，此时，我们要么修正实现代码，要么修正测试代码，直到测试通过为止。</p>
<p>使用浮点数时，由于浮点数无法精确的进行比较，因此，我们需要调用<code>assertEquals(double expected, double actual, double delta)</code>这个重载方法，指定一个误差值：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java">assertEquals(<span class="hljs-number">0.1</span>, Math.abs(<span class="hljs-number">1</span> - <span class="hljs-number">9</span> / <span class="hljs-number">10.0</span>), <span class="hljs-number">0.0000001</span>);<br></code></pre></td></tr></tbody></table></figure>

<h3 id="单元测试的好处"><a href="#单元测试的好处" class="headerlink" title="单元测试的好处"></a>单元测试的好处</h3><p>单元测试可以确保单个方法按照正确预期运行，如果修改了某个方法的代码，只需确保其对应的单元测试通过，即可认为改动正确。此外，测试代码本身可以作为示例代码，用来演示如何调用该方法。</p>
<p>使用JUnit进行单元测试，我们可以使用断言（Assertion）来测试期望结果，可以方便地组织和运行测试，并方便的查看测试结果。另外，JUnit既可以直接在IDE中运行，也可以方便的集成到Maven这些自动化工具中运行。</p>
<p>在编写单元测试的时候，我们要遵循一定的规范：</p>
<p>一是单元测试代码本身必须非常简单，能一下看明白，绝不能再为测试代码编写测试；</p>
<p>二是每个单元测试应当互相独立，不依赖运行的顺序；</p>
<p>三是测试时不但要覆盖常用测试用例，还要特别注意测试边界条件，例如输入为<code>0</code>，<code>null</code>，空白字符串<code>" "</code>等情况。</p>
<h2 id="使用Fixture"><a href="#使用Fixture" class="headerlink" title="使用Fixture"></a>使用Fixture</h2><p>在一个单元测试中，我们经常编写多个<code>@Test</code>方法，来分组、分类对目标代码进行测试。在测试的时候，我们经常遇到一个对象需要初始化，测试完可能还需要清理的情况。如果每个@Test方法都写一遍这样的重复代码，显然比较麻烦。JUint提供了编写测试前准备、测试后清理的固定代码，我们称之为Fixture。</p>
<p>这个类的功能很简单，但是测试的时候，我们要先初始化对象，我们不必在每个测试方法中都写上初始化代码，而是通过<code>@BeforeEach</code>来初始化，通过<code>@AfterEach</code>来清理资源：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CalculatorTest</span> </span>{<br><br>    Calculator calculator;<br><br>    <span class="hljs-meta">@BeforeEach</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">setUp</span><span class="hljs-params">()</span> </span>{<br>        <span class="hljs-keyword">this</span>.calculator = <span class="hljs-keyword">new</span> Calculator();<br>    }<br><br>    <span class="hljs-meta">@AfterEach</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">tearDown</span><span class="hljs-params">()</span> </span>{<br>        <span class="hljs-keyword">this</span>.calculator = <span class="hljs-keyword">null</span>;<br>    }<br><br>    <span class="hljs-meta">@Test</span><br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">testAdd</span><span class="hljs-params">()</span> </span>{<br>        assertEquals(<span class="hljs-number">100</span>, <span class="hljs-keyword">this</span>.calculator.add(<span class="hljs-number">100</span>));<br>        assertEquals(<span class="hljs-number">150</span>, <span class="hljs-keyword">this</span>.calculator.add(<span class="hljs-number">50</span>));<br>        assertEquals(<span class="hljs-number">130</span>, <span class="hljs-keyword">this</span>.calculator.add(-<span class="hljs-number">20</span>));<br>    }<br><br>    <span class="hljs-meta">@Test</span><br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">testSub</span><span class="hljs-params">()</span> </span>{<br>        assertEquals(-<span class="hljs-number">100</span>, <span class="hljs-keyword">this</span>.calculator.sub(<span class="hljs-number">100</span>));<br>        assertEquals(-<span class="hljs-number">150</span>, <span class="hljs-keyword">this</span>.calculator.sub(<span class="hljs-number">50</span>));<br>        assertEquals(-<span class="hljs-number">130</span>, <span class="hljs-keyword">this</span>.calculator.sub(-<span class="hljs-number">20</span>));<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>在<code>CalculatorTest</code>测试中，有两个标记为<code>@BeforeEach</code>和<code>@AfterEach</code>的方法，它们会在运行每个<code>@Test</code>方法前后自动运行。</p>
<p>上面的测试代码在JUnit中运行顺序如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">for</span> (Method testMethod : findTestMethods(CalculatorTest.class)) {<br>    <span class="hljs-keyword">var</span> test = <span class="hljs-keyword">new</span> CalculatorTest(); <span class="hljs-comment">// 创建Test实例</span><br>    invokeBeforeEach(test);<br>        invokeTestMethod(test, testMethod);<br>    invokeAfterEach(test);<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>可见，<code>@BeforeEach</code>和<code>@AfterEach</code>会“环绕”在每个<code>@Test</code>方法前后。</p>
<p>还有一些资源初始化和清理可能更加繁琐，而且会耗费较长时间，例如初始化数据库。JUnit还提供了<code>@BeforeAll</code>和<code>@AfterAll</code>，它们在运行所有<code>@Test</code>前后运行。顺序如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs java">invokeBeforeAll(CalculatorTest.class);<br><span class="hljs-keyword">for</span> (Method testMethod : findTestMethods(CalculatorTest.class)) {<br>    <span class="hljs-keyword">var</span> test = <span class="hljs-keyword">new</span> CalculatorTest(); <span class="hljs-comment">// 创建Test实例</span><br>    invokeBeforeEach(test);<br>        invokeTestMethod(test, testMethod);<br>    invokeAfterEach(test);<br>}<br>invokeAfterAll(CalculatorTest.class);<br></code></pre></td></tr></tbody></table></figure>

<p>因为<code>@BeforeAll</code>和<code>@AfterAll</code>在所有<code>@Test</code>方法运行前后仅运行一次，因此，他们只能初始化静态变量。例如：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DatabaseTest</span> </span>{<br>    <span class="hljs-keyword">static</span> Database db;<br><br>    <span class="hljs-meta">@BeforeAll</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">initDatabase</span><span class="hljs-params">()</span> </span>{<br>        db = createDb(...);<br>    }<br>    <br>    <span class="hljs-meta">@AfterAll</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">dropDatabase</span><span class="hljs-params">()</span> </span>{<br>        ...<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>事实上，<code>@BeforeAll</code>和<code>@AfterAll</code>也只能标注在静态方法上。</p>
<p>因此，我们总结出编写Fixture的套路如下：</p>
<ol>
<li>对于实例变量，在<code>@BeforeEach</code>中初始化，在<code>@AfterEach</code>中清理，它们在各个<code>@Test</code>方法中互不影响，因为是不同的实例；</li>
<li>对于静态变量，在<code>@BeforeAll</code>中初始化，在<code>@AfterAll</code>中清理，它们在各个<code>@Test</code>方法中均是唯一实例，会影响各个<code>@Test</code>方法。</li>
</ol>
<p>大多数情况下，使用<code>@BeforeEach</code>和<code>@AfterEach</code>就足够了。只有某些测试资源初始化耗费时间太长，以至于我们不得不尽量“复用”时才会用到<code>@BeforeAll</code>和<code>@AfterAll</code>。</p>
<p>最后，注意到每次运行一个<code>@Test</code>方法前，JUnit首先创建一个<code>XxxTest</code>实例，因此，每个<code>@Test</code>方法内部的成员变量都是独立的，不能也无法把成员变量的状态从一个<code>@Test</code>方法带到另一个<code>@Test</code>方法。</p>
<h2 id="异常测试"><a href="#异常测试" class="headerlink" title="异常测试"></a>异常测试</h2><p>在Java程序中，异常处理是非常重要的。我们自己编写的方法，也经常抛出异常。对于可能抛出异常进行测试，本身就是测试的重要环节。因此，在编写JUnit测试时，除了正常的输入输出，我们还要特定针对可能导致异常的情况进行测试。</p>
<p>我们仍然用<code>Factorial</code>举例：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Factorial</span> </span>{    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">long</span> <span class="hljs-title">fact</span><span class="hljs-params">(<span class="hljs-keyword">long</span> n)</span> </span>{        <span class="hljs-keyword">if</span> (n &lt; <span class="hljs-number">0</span>) {            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IllegalArgumentException();        }        <span class="hljs-keyword">long</span> r = <span class="hljs-number">1</span>;        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">long</span> i = <span class="hljs-number">1</span>; i &lt;= n; i++) {            r = r * i;        }        <span class="hljs-keyword">return</span> r;    }}<br></code></pre></td></tr></tbody></table></figure>

<p>在方法入口，我们增加了对参数<code>n</code>的检查，如果为负数，则直接抛出<code>IllegalArgumentException</code>。</p>
<p>现在，我们希望对异常进行测试。在JUnit测试中，我们可以编写一个<code>@Test</code>方法专门测试异常：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Testvoid</span> testNegative() {    assertThrows(IllegalArgumentException.class, <span class="hljs-keyword">new</span> Executable() {        <span class="hljs-meta">@Override</span>        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">execute</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Throwable </span>{            Factorial.fact(-<span class="hljs-number">1</span>);        }    });}<br></code></pre></td></tr></tbody></table></figure>

<p>JUnit提供<code>assertThrows()</code>来期望捕获一个指定的异常。第二个参数<code>Executable</code>封装了我们要执行的会产生异常的代码。当我们执行<code>Factorial.fact(-1)</code>时，必定抛出<code>IllegalArgumentException</code>。<code>assertThrows()</code>在捕获到指定异常时表示通过测试，未捕获到异常，或者捕获到的异常类型不对，均表示测试失败。</p>
<p>有些同学可能觉得编写一个<code>Executable</code>的匿名类太繁琐了，实际上，Java 8开始引入了函数式编程，所有单方法接口都可以简写如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Testvoid</span> testNegative() {    assertThrows(IllegalArgumentException.class, () -&gt; {        Factorial.fact(-<span class="hljs-number">1</span>);    });}<br></code></pre></td></tr></tbody></table></figure>

<p>上述奇怪的<code>-&gt;</code>语法就是函数式接口的实现代码，我们会在后面详细介绍。现在，我们只需要通过这种固定的代码编写能抛出异常的语句即可。</p>
<h2 id="条件测试"><a href="#条件测试" class="headerlink" title="条件测试"></a>条件测试</h2><p>在运行测试的时候，有时我们需要排除某些@Test方法，不要让它运行。这时，我们就可以给他标记一个<code>@Disable</code>：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Disabled</span><span class="hljs-meta">@Testvoid</span> testBug101() {    <span class="hljs-comment">// 这个测试不会运行}</span><br></code></pre></td></tr></tbody></table></figure>

<p>为什么我们不直接注释掉@Test，而是加一个@Disable呢？这是因为注释掉@Test，JUnit就不知道这是一个测试方法，而加上@Disable，JUnit仍然识别出这是个测试方法，只是暂时不运行，他会在测试结果中显示：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java">Tests run: <span class="hljs-number">68</span>, Failures: <span class="hljs-number">2</span>, Errors: <span class="hljs-number">0</span>, Skipped: <span class="hljs-number">5</span><br></code></pre></td></tr></tbody></table></figure>

<p>类似<code>@Disable</code>这种注解就称为条件测试，JUnit根据不同的条件注解，决定是否运行当前的@Test方法。</p>
<p>我们来看一个例子：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Config</span> </span>{    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">getConfigFile</span><span class="hljs-params">(String filename)</span> </span>{        String os = System.getProperty(<span class="hljs-string">"os.name"</span>).toLowerCase();        <span class="hljs-keyword">if</span> (os.contains(<span class="hljs-string">"win"</span>)) {            <span class="hljs-keyword">return</span> <span class="hljs-string">"C:\\"</span> + filename;        }        <span class="hljs-keyword">if</span> (os.contains(<span class="hljs-string">"mac"</span>) || os.contains(<span class="hljs-string">"linux"</span>) || os.contains(<span class="hljs-string">"unix"</span>)) {            <span class="hljs-keyword">return</span> <span class="hljs-string">"/usr/local/"</span> + filename;        }        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnsupportedOperationException();    }}<br></code></pre></td></tr></tbody></table></figure>

<p>我们想要测试<code>getConfigFile()</code>这个方法，但是在Windows上跑，和在Linux上跑的代码路径不同，因此，针对两个系统的测试方法，其中一个只能在Windows上跑，另一个只能在Mac/Linux上跑：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Testvoid</span> testWindows() {    assertEquals(<span class="hljs-string">"C:\\test.ini"</span>, config.getConfigFile(<span class="hljs-string">"test.ini"</span>));}<span class="hljs-meta">@Testvoid</span> testLinuxAndMac() {    assertEquals(<span class="hljs-string">"/usr/local/test.cfg"</span>, config.getConfigFile(<span class="hljs-string">"test.cfg"</span>));}<br></code></pre></td></tr></tbody></table></figure>

<p>因此，我们给两个测试方法分别加上条件如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Test</span><span class="hljs-meta">@EnabledOnOs(OS.WINDOWS)</span><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">testWindows</span><span class="hljs-params">()</span> </span>{    assertEquals(<span class="hljs-string">"C:\\test.ini"</span>, config.getConfigFile(<span class="hljs-string">"test.ini"</span>));}<span class="hljs-meta">@Test</span><span class="hljs-meta">@EnabledOnOs({ OS.LINUX, OS.MAC })</span><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">testLinuxAndMac</span><span class="hljs-params">()</span> </span>{    assertEquals(<span class="hljs-string">"/usr/local/test.cfg"</span>, config.getConfigFile(<span class="hljs-string">"test.cfg"</span>));}<br></code></pre></td></tr></tbody></table></figure>

<p><code>@EnableOnOs</code>就是一个条件测试判断。</p>
<p>不在Windows平台执行的测试，可以加上@DisableOnOs(OS.WINDOWS)。</p>
<p>只能在Java 9及更高版本执行的测试，可以加上@DisableOnJre(JRE.JAVA_8)。</p>
<p>只能在64位操作系统上执行的测试，可以用@EnableIfSystemProperty判断。</p>
<p>需要传入环境变量DEBUG=true才能执行的测试，可以用@EnableIfEnvironmentVariable。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Test</span><span class="hljs-meta">@EnabledIfEnvironmentVariable(named = "DEBUG", matches = "true")</span><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">testOnlyOnDebugMode</span><span class="hljs-params">()</span> </span>{    <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> this test is only run on DEBUG=true}</span><br></code></pre></td></tr></tbody></table></figure>

<p>当我们在JUnit中运行所有的测试的时候，JUnit会给出执行的结果。在IDE中，我们很容易地看到没有执行的测试。</p>
<h2 id="参数化测试"><a href="#参数化测试" class="headerlink" title="参数化测试"></a>参数化测试</h2><p>如果待测试的输入和输出是一组数据：可以把测试数据组织起来，用不同的测试数据调用相同的测试方法。参数化测试和普通测试不同的地方在于，一个测试方法需要至少接受一个参数，然后，传入一组参数反复运行。</p>
<p>JUnit提供了一个<code>@ParameterizedTest</code>注解，用来进行参数化测试。</p>
<p>假设我们想对<code>Math.abs()</code>进行测试，先用一组正数进行测试：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@ParameterizedTest</span><span class="hljs-meta">@ValueSource(ints = { 0, 1, 5, 100 })</span><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">testAbs</span><span class="hljs-params">(<span class="hljs-keyword">int</span> x)</span> </span>{    assertEquals(x, Math.abs(x));}<br></code></pre></td></tr></tbody></table></figure>

<p>再用一组负数进行测试：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@ParameterizedTest</span><span class="hljs-meta">@ValueSource(ints = { -1, -5, -100 })</span><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">testAbsNegative</span><span class="hljs-params">(<span class="hljs-keyword">int</span> x)</span> </span>{    assertEquals(-x, Math.abs(x));}<br></code></pre></td></tr></tbody></table></figure>

<p>注意到参数化测试的注解是<code>@ParameterizedTest</code>，而不是普通的<code>@Test</code>。</p>
<p>实际的测试场景往往没有那么简单。</p>
<p>假设我们自己编写了一个<code>StringUtils.capitalize()</code>方法，它会把字符串的第一个字母变为大写，后续字母变为小写：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StringUtils</span> </span>{    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> String <span class="hljs-title">capitalize</span><span class="hljs-params">(String s)</span> </span>{        <span class="hljs-keyword">if</span> (s.length() == <span class="hljs-number">0</span>) {            <span class="hljs-keyword">return</span> s;        }        <span class="hljs-keyword">return</span> Character.toUpperCase(s.charAt(<span class="hljs-number">0</span>)) + s.substring(<span class="hljs-number">1</span>).toLowerCase();    }}<br></code></pre></td></tr></tbody></table></figure>

<p>要用参数化测试的方法来测试，我们不但要给出输入，还要给出预期输出。因此，测试方法至少需要接收两个参数：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@ParameterizedTestvoid</span> testCapitalize(String input, String result) {    assertEquals(result, StringUtils.capitalize(input));}<br></code></pre></td></tr></tbody></table></figure>

<p>现在问题来了：参数如何传入？</p>
<p>最简单的方法是通过<code>@MethodSource</code>注解，它允许我们编写一个同名的静态方法来提供测试参数：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@ParameterizedTest</span><span class="hljs-meta">@MethodSourcevoid</span> testCapitalize(String input, String result) {    assertEquals(result, StringUtils.capitalize(input));}<span class="hljs-function"><span class="hljs-keyword">static</span> List&lt;Arguments&gt; <span class="hljs-title">testCapitalize</span><span class="hljs-params">()</span> </span>{    <span class="hljs-keyword">return</span> List.of( <span class="hljs-comment">// arguments:            Arguments.arguments("abc", "Abc"), //            Arguments.arguments("APPLE", "Apple"), //            Arguments.arguments("gooD", "Good"));}</span><br></code></pre></td></tr></tbody></table></figure>

<p>上面的代码很容易理解：静态方法<code>testCapitalize()</code>返回了一组测试参数，每个参数都包含两个<code>String</code>，正好作为测试方法的两个参数传入。如果静态方法和测试方法的名称不同，<code>@MethodSource</code>也允许指定方法名，但使用默认同名方法最简便。</p>
<p>另一种传入测试参数的方法是使用<code>@CsvSource</code>，它的每一个字符串表示一行，一行包含的若干参数用<code>,</code>分隔，因此，上述测试又可以改写如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@ParameterizedTest</span><span class="hljs-meta">@CsvSource({ "abc, Abc", "APPLE, Apple", "gooD, Good" })</span><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">testCapitalize</span><span class="hljs-params">(String input, String result)</span> </span>{    assertEquals(result, StringUtils.capitalize(input));}<br></code></pre></td></tr></tbody></table></figure>

<p>如果有成百上千的测试输入，那么，直接写<code>@CsvSource</code>就很不方便。这个时候，我们可以把测试数据提到一个独立的CSV文件中，然后标注上<code>@CsvFileSource</code>：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@ParameterizedTest</span><span class="hljs-meta">@CsvFileSource(resources = { "/test-capitalize.csv" })</span><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">testCapitalizeUsingCsvFile</span><span class="hljs-params">(String input, String result)</span> </span>{    assertEquals(result, StringUtils.capitalize(input));}<br></code></pre></td></tr></tbody></table></figure>

<p>JUnit只在classpath中查找指定的CSV文件，因此，<code>test-capitalize.csv</code>这个文件要放到<code>test</code>目录下，内容如下：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs plaintext">apple, AppleHELLO, HelloJUnit, JunitreSource, Resource<br></code></pre></td></tr></tbody></table></figure>

</body></html>
    
    </div>
    
    
    <div class="columns is-mobile is-multiline article-nav">
        <span class="column is-12-mobile is-half-desktop  article-nav-prev">
            
            <a href="/Study/Java/zh-cn/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F/">正则表达式</a>
            
        </span>
        <span class="column is-12-mobile is-half-desktop  article-nav-next">
            
            <a href="/Study/Java/zh-cn/%E6%97%A5%E6%9C%9F%E4%B8%8E%E6%97%B6%E9%97%B4/">日期与时间</a>
            
        </span>
    </div>
    
</article>


<div class="sharebox">
    
<div class="sharethis-inline-share-buttons"></div>
<script type='text/javascript' src='//platform-api.sharethis.com/js/sharethis.js#property=608c1408daac690012507aa2&amp;product=sop' async='async'></script>

</div>



    </div>
</section>
    <footer class="footer">
    <div class="container">
        <div class="columns content">
            <div class="column is-narrow has-text-centered">
                &copy; 2021 wanzixin&nbsp;
                Powered by <a href="http://hexo.io/" target="_blank">Hexo</a> & <a
                        target="_blank" rel="noopener" href="http://github.com/ppoffice/hexo-theme-minos">Minos</a>
            </div>
            <div class="column is-hidden-mobile"></div>

            
            <div class="column is-narrow">
                <div class="columns is-mobile is-multiline is-centered">
                
                    
                <a class="column is-narrow has-text-black" title="GitHub" target="_blank" rel="noopener" href="https://github.com/ppoffice/hexo-theme-minos">
                    
                    GitHub
                    
                </a>
                
                </div>
            </div>
            
            
<div class="column is-narrow has-text-centered">
    <div class="dropdown is-up is-right is-hoverable" style="margin-top: -0.2em;">
        <div class="dropdown-trigger">
            <button class="button is-small" aria-haspopup="true" aria-controls="dropdown-menu7">
                <span class="icon">
                    <i class="fas fa-globe"></i>
                </span>
                <span>简体中文</span>
                <span class="icon is-small">
            <i class="fas fa-angle-down" aria-hidden="true"></i>
          </span>
            </button>
        </div>
        <div class="dropdown-menu has-text-left" role="menu">
            <div class="dropdown-content">
            
                <a href="/Study/Java/zh-cn/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/" class="dropdown-item">
                    English
                </a>
            
                <a href="/zh-cn/Study/Java/zh-cn/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/" class="dropdown-item">
                    简体中文
                </a>
            
            </div>
        </div>
    </div>
</div>

        </div>
    </div>
</footer>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment-with-locales.min.js"></script>

<!-- test if the browser is outdated -->
<div id="outdated">
    <h6>Your browser is out-of-date!</h6>
    <p>Update your browser to view this website correctly. <a id="btnUpdateBrowser" target="_blank" rel="noopener" href="http://outdatedbrowser.com/">Update my browser now </a></p>
    <p class="last"><a href="#" id="btnCloseUpdateBrowser" title="Close">&times;</a></p>
</div>
<script src="//cdnjs.cloudflare.com/ajax/libs/outdated-browser/1.1.5/outdatedbrowser.min.js"></script>
<script>
    $(document).ready(function () {
        // plugin function, place inside DOM ready function
        outdatedBrowser({
            bgColor: '#f25648',
            color: '#ffffff',
            lowerThan: 'flex'
        })
    });
</script>

<script>
    window.FontAwesomeConfig = {
        searchPseudoElements: true
    }
    moment.locale("zh-CN");
</script>


    
    
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script>
    MathJax.Hub.Config({
        "HTML-CSS": {
            matchFontHeight: false
        },
        SVG: {
            matchFontHeight: false
        },
        CommonHTML: {
            matchFontHeight: false
        },
        tex2jax: {
            inlineMath: [
                ['$','$'],
                ['\\(','\\)']
            ]
        }
    });
</script>

    
    
    
    
<script src="//cdnjs.cloudflare.com/ajax/libs/lightgallery/1.6.8/js/lightgallery-all.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/justifiedGallery/3.6.5/js/jquery.justifiedGallery.min.js"></script>
<script>
    (function ($) {
        $(document).ready(function () {
            if (typeof($.fn.lightGallery) === 'function') {
                $('.article.gallery').lightGallery({ selector: '.gallery-item' });
            }
            if (typeof($.fn.justifiedGallery) === 'function') {
                $('.justified-gallery').justifiedGallery();
            }
        });
    })(jQuery);
</script>

    
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js"></script>
    <style>
        .hljs {
            position: relative;
        }

        .hljs .clipboard-btn {
            float: right;
            color: #9a9a9a;
            background: none;
            border: none;
            cursor: pointer;
        }

        .hljs .clipboard-btn:hover {
          color: #8a8a8a;
        }

        .hljs > .clipboard-btn {
            display: none;
            position: absolute;
            right: 4px;
            top: 4px;
        }

        .hljs:hover > .clipboard-btn {
            display: inline;
        }

        .hljs > figcaption > .clipboard-btn {
            margin-right: 4px;
        }
    </style>
    <script>
      $(document).ready(function () {
        $('figure.hljs').each(function(i, figure) {
          var codeId = 'code-' + i;
          var code = figure.querySelector('.code');
          var copyButton = $('<button>Copy <i class="far fa-clipboard"></i></button>');
          code.id = codeId;
          copyButton.addClass('clipboard-btn');
          copyButton.attr('data-clipboard-target-id', codeId);

          var figcaption = figure.querySelector('figcaption');

          if (figcaption) {
            figcaption.append(copyButton[0]);
          } else {
            figure.prepend(copyButton[0]);
          }
        })

        var clipboard = new ClipboardJS('.clipboard-btn', {
          target: function(trigger) {
            return document.getElementById(trigger.getAttribute('data-clipboard-target-id'));
          }
        });
        clipboard.on('success', function(e) {
          e.clearSelection();
        })
      })
    </script>

    
    

    



<script src="/js/script.js"></script>


    
    <div class="searchbox ins-search">
    <div class="searchbox-mask"></div>
    <div class="searchbox-container ins-search-container">
        <div class="searchbox-input-wrapper">
            <input type="text" class="searchbox-input ins-search-input" placeholder="站内搜索" />
            <span class="searchbox-close ins-close ins-selectable"><i class="fa fa-times-circle"></i></span>
        </div>
        <div class="searchbox-result-wrapper ins-section-wrapper">
            <div class="ins-section-container"></div>
        </div>
    </div>
</div>
<script>
    (function (window) {
        var INSIGHT_CONFIG = {
            TRANSLATION: {
                POSTS: '文章',
                PAGES: '页面',
                CATEGORIES: '分类',
                TAGS: '标签',
                UNTITLED: '(无标题)',
            },
            CONTENT_URL: '/content.zh-cn.json',
        };
        window.INSIGHT_CONFIG = INSIGHT_CONFIG;
    })(window);
</script>

<script src="/js/insight.js"></script>

    
</body>
</html>